#include <jni.h>
#include <stdlib.h>
#include <stdio.h>

#include <ByteStream.h>
#include <DjVuFile.h>
#include <GBitmap.h>

#include <DjVuImage.h>
#include <DjVmDir.h>
#include <DjVuNavDir.h>
#include <DjVuDocument.h>
#include <DjVuFileCache.h>
#include <DjVmNav.h>
#include <DjVuText.h>

#include <ddjvuapi.h>

#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/bitmap.h>

class JavaInputStream : public ByteStream {
    JNIEnv *env;
    jmethodID methodRead;
    jmethodID methodTell;
    jobject thiz;

public:
    JavaInputStream(JNIEnv *env, jobject thiz) {
        this->env = env;
        this->thiz = thiz;
        jclass cls = env->GetObjectClass(thiz);
        this->methodRead = env->GetMethodID(cls, "read", "(I)[B");
        if (this->methodRead == NULL) {
            env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to find read");
            return;
        }
        this->methodTell = env->GetMethodID(cls, "tell", "()J");
        if (this->methodTell == NULL) {
            env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to find tell");
            return;
        }
    }

    ~JavaInputStream() {
    }

    virtual size_t read(void *buffer, size_t sz) {
        jbyteArray ret = (jbyteArray) env->CallObjectMethod(thiz, methodRead, sz);
        if (ret == NULL)
            return 0;
        jint l = env->GetArrayLength(ret);
        env->GetByteArrayRegion(ret, 0, l, (jbyte *) buffer);
        env->DeleteLocalRef(ret);
        return (size_t) l;
    }

    virtual long tell(void) const {
        return env->CallIntMethod(thiz, methodTell);
    }
};

typedef struct {
    GP<DjVuFileCache> cache;
    GP<DjVuDocument> doc;
} djvu, *djvu_t;

uint16_t rgb_to_565(unsigned char R8, unsigned char G8, unsigned char B8) {
    unsigned char R5 = (R8 * 249 + 1014) >> 11;
    unsigned char G6 = (G8 * 253 + 505) >> 10;
    unsigned char B5 = (B8 * 249 + 1014) >> 11;
    return (R5 << 11) | (G6 << 5) | (B5);
}

static uint32_t rgb_to_32(unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
    return (a << 24) | (b << 16) | (g << 8) | (r);
}

static void fmt_convert_row(unsigned char *p, unsigned char g[256][4], int w, jint *buf) {
    while (--w >= 0) {
        buf[0] = rgb_to_32(g[*p][2], g[*p][1], g[*p][0], 255);
        buf += 1;
        p += 1;
    }
}

static void fmt_convert(GBitmap *bm, unsigned char *buffer, int rowsize) {
    int w = bm->columns();
    int h = bm->rows();
    int m = bm->get_grays();
    int i;
    unsigned char g[256][4];
    const GPixel &wh = GPixel::WHITE;
    for (i = 0; i < m; i++) {
        g[i][0] = wh.b - (i * wh.b + (m - 1) / 2) / (m - 1);
        g[i][1] = wh.g - (i * wh.g + (m - 1) / 2) / (m - 1);
        g[i][2] = wh.r - (i * wh.r + (m - 1) / 2) / (m - 1);
        g[i][3] = (5 * g[i][2] + 9 * g[i][1] + 2 * g[i][0]) >> 4;
    }
    for (i = m; i < 256; i++)
        g[i][0] = g[i][1] = g[i][2] = g[i][3] = 0;

    for (int r = h - 1; r >= 0; r--, buffer += rowsize)
        fmt_convert_row((*bm)[r], g, w, (jint *) buffer);
}

static void fmt_convert_row_565(unsigned char *p, unsigned char g[256][4], int w, uint16_t *buf) {
    while (--w >= 0) {
        buf[0] = rgb_to_565(g[*p][2], g[*p][1], g[*p][0]);
        buf += 1;
        p += 1;
    }
}

static void fmt_convert_565(GBitmap *bm, unsigned char *buffer, int rowsize) {
    int w = bm->columns();
    int h = bm->rows();
    int m = bm->get_grays();
    int i;
    unsigned char g[256][4];
    const GPixel &wh = GPixel::WHITE;
    for (i = 0; i < m; i++) {
        g[i][0] = wh.b - (i * wh.b + (m - 1) / 2) / (m - 1);
        g[i][1] = wh.g - (i * wh.g + (m - 1) / 2) / (m - 1);
        g[i][2] = wh.r - (i * wh.r + (m - 1) / 2) / (m - 1);
        g[i][3] = (5 * g[i][2] + 9 * g[i][1] + 2 * g[i][0]) >> 4;
    }
    for (i = m; i < 256; i++)
        g[i][0] = g[i][1] = g[i][2] = g[i][3] = 0;

    for (int r = h - 1; r >= 0; r--, buffer += rowsize)
        fmt_convert_row_565((*bm)[r], g, w, (uint16_t *) buffer);
}

static void bmp_convert(GP<GPixmap> pm, unsigned char *buf, int rowsize) {
    int w = pm->columns();
    int h = pm->rows();
    for (int y = 0; y < h; y++) {
        GPixel *row = (*pm)[y];
        for (int x = 0; x < w; x++) {
            GPixel p = row[x];
            int yy = h - y - 1;
            jint *drow = ((jint *) (buf + yy * rowsize)) + x;
            *drow = rgb_to_32(p.r, p.g, p.b, 255);
        }
    }
}

static void bmp_convert_565(GP<GPixmap> pm, unsigned char *buf, int rowsize) {
    int w = pm->columns();
    int h = pm->rows();
    for (int y = 0; y < h; y++) {
        GPixel *row = (*pm)[y];
        for (int x = 0; x < w; x++) {
            GPixel p = row[x];
            int yy = h - y - 1;
            uint16_t *drow = ((uint16_t *) (buf + yy * rowsize)) + x;
            *drow = rgb_to_565(p.r, p.g, p.b);
        }
    }
}

extern "C" {

JNIEXPORT void JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_create(JNIEnv *env, jobject thiz) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");

    djvu_t djvu = new ::djvu();
    env->SetLongField(thiz, fid, (jlong) djvu);

    GP<ByteStream> b = GP<ByteStream>(new JavaInputStream(env, thiz));
    if (env->ExceptionOccurred() != NULL)
        return;
    djvu->cache = DjVuFileCache::create();
    djvu->doc = DjVuDocument::create(b, 0, djvu->cache);
    if (!djvu->doc) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "doc null");
        return;
    }
    djvu->doc->wait_get_pages_num(); // wait until doc loaded
}

JNIEXPORT void JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_setCacheSize(JNIEnv *env, jobject thiz, jint size) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    djvu->cache->set_max_size(size);
}

JNIEXPORT jint JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_getPagesCount(JNIEnv *env, jobject thiz) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    return djvu->doc->get_pages_num();
}

JNIEXPORT jstring JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_getMeta(JNIEnv *env, jobject thiz, jstring meta) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    GP<DjVmDir> dir = djvu->doc->get_djvm_dir();
    GP<DjVmDir::File> file = dir->get_shared_anno_file();
    if (!file)
        return NULL;
    const GUTF8String &id = file->get_load_name();
    GP<DjVuFile> df = djvu->doc->get_djvu_file(id);
    if (!df)
        return NULL;
    GP<ByteStream> bs = df->get_anno();
    if (!bs)
        return NULL;
    GP<DjVuAnno> anno = DjVuAnno::create();
    anno->decode(bs);
    const char *m = env->GetStringUTFChars(meta, 0);
    const GUTF8String str = anno->ant->metadata[m];
    env->ReleaseStringUTFChars(meta, m);
    return env->NewStringUTF((const char *) str);
}

JNIEXPORT jobject JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_getText(JNIEnv *env, jobject thiz, jint page, jint type) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    GP<DjVmDir> dir = djvu->doc->get_djvm_dir();
    int pos = dir->get_page_pos(page);
    GPList<DjVmDir::File> pages = dir->get_files_list();
    GP<DjVmDir::File> file = pages[pages.nth(pos)];
    if (!file)
        return NULL;
    const GUTF8String &id = file->get_load_name();
    GP<DjVuFile> df = djvu->doc->get_djvu_file(id);
    if (!df)
        return NULL;
    GP<ByteStream> bs = df->get_text();
    if (!bs)
        return NULL;
    GP<DjVuText> text = DjVuText::create();
    text->decode(bs);

    jclass stringClass = env->FindClass("java/lang/String");
    jclass rectClass = env->FindClass("android/graphics/Rect");
    jmethodID rectInit = env->GetMethodID(rectClass, "<init>", "(IIII)V");
    jclass textClass = env->FindClass("com/github/axet/djvulibre/DjvuLibre$Text");
    jmethodID textInit = env->GetMethodID(textClass, "<init>",
                                          "([Ljava/lang/String;[Landroid/graphics/Rect;)V");

    GList<DjVuTXT::Zone *> zone_list;
    text->txt->get_zones(type, &text->txt->page_zone, zone_list);

    jobjectArray rr = env->NewObjectArray(zone_list.size(), rectClass, 0);
    jobjectArray ss = env->NewObjectArray(zone_list.size(), stringClass, 0);

    int e = 0;
    for (GPosition i = zone_list.firstpos(); !!i; ++i, ++e) {
        DjVuTXT::Zone *zone = zone_list[i];
        jobject r = env->NewObject(rectClass, rectInit, zone->rect.xmin, zone->rect.ymin,
                                   zone->rect.xmax, zone->rect.ymax);
        GUTF8String sub = text->txt->textUTF8.substr(zone->text_start, zone->text_length);
        jstring s = env->NewStringUTF(sub);
        env->SetObjectArrayElement(rr, e, r);
        env->SetObjectArrayElement(ss, e, s);
        env->DeleteLocalRef(r);
        env->DeleteLocalRef(s);
    }

    return env->NewObject(textClass, textInit, ss, rr);
}

JNIEXPORT jobject JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_getPageInfo(JNIEnv *env, jobject thiz, jint pageIndex) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    GP<DjVuImage> image = djvu->doc->get_page(pageIndex);
    if (!image) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "image null");
        return NULL;
    }
    jclass pageClass = env->FindClass("com/github/axet/djvulibre/DjvuLibre$Page");
    if (pageClass == NULL) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to find page class");
        return NULL;
    }
    jmethodID c = env->GetMethodID(pageClass, "<init>", "(III)V");
    if (c == NULL) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to create page");
        return NULL;
    }
    return env->NewObject(pageClass, c, image->get_width(), image->get_height(), image->get_dpi());
}

JNIEXPORT void JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_renderPage(JNIEnv *env, jobject thiz, jobject bm,
                                                    jint pageIndex, jint sx, jint sy, jint sw,
                                                    jint sh, jint dx, jint dy, jint dw, jint dh) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);

    GP<DjVuImage> image = djvu->doc->get_page(pageIndex);
    if (!image) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to create image");
        return;
    }
    image->wait_for_complete_decode();
    if (!image->get_info()) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to get image info");
        return;
    }

    AndroidBitmapInfo info;
    int ret;
    if ((ret = AndroidBitmap_getInfo(env, bm, &info)) < 0) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), strerror(ret * -1));
        return;
    }

    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 &&
        info.format != ANDROID_BITMAP_FORMAT_RGB_565) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "not supported format");
        return;
    }

    unsigned char *buf;
    if ((ret = AndroidBitmap_lockPixels(env, bm, (void **) &buf)) != 0) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), strerror(ret * -1));
        return;
    }

    GRect all = GRect(sx, sy, sw, sh);
    GRect dst = GRect(dx, dy, dw, dh);

    GP<GPixmap> pm = image->get_pixmap(dst, all);
    if (pm) {
        switch (info.format) {
            case ANDROID_BITMAP_FORMAT_RGBA_8888:
                bmp_convert(pm, buf, info.stride);
                break;
            case ANDROID_BITMAP_FORMAT_RGB_565:
                bmp_convert_565(pm, buf, info.stride);
                break;
        }
    } else {
        GP<GBitmap> bm = image->get_bitmap(dst, all);
        if (bm) {
            switch (info.format) {
                case ANDROID_BITMAP_FORMAT_RGBA_8888:
                    fmt_convert(bm, buf, info.stride);
                    break;
                case ANDROID_BITMAP_FORMAT_RGB_565:
                    fmt_convert_565(bm, buf, info.stride);
                    break;
            }
        }
    }
    AndroidBitmap_unlockPixels(env, bm);
}

JNIEXPORT void JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_close(JNIEnv *env, jobject thiz) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    if (djvu != 0) {
        delete djvu;
        env->SetLongField(thiz, fid, 0);
    }
}

JNIEXPORT jobjectArray JNICALL
Java_com_github_axet_djvulibre_DjvuLibre_getBookmarks(JNIEnv *env, jobject thiz) {
    jclass cls = env->GetObjectClass(thiz);
    jfieldID fid = env->GetFieldID(cls, "handle", "J");
    djvu_t djvu = (djvu_t) env->GetLongField(thiz, fid);
    GP<DjVmNav> nav = djvu->doc->get_djvm_nav();
    jclass pageClass = env->FindClass("com/github/axet/djvulibre/DjvuLibre$Bookmark");
    if (pageClass == NULL) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to find page class");
        return NULL;
    }
    jmethodID c = env->GetMethodID(pageClass, "<init>", "(ILjava/lang/String;I)V");
    if (c == NULL) {
        env->ThrowNew(env->FindClass("java/lang/RuntimeException"), "Unable to create page");
        return NULL;
    }
    if (!nav)
        return 0;
    int count = nav->getBookMarkCount();
    jobjectArray ar = (jobjectArray) env->NewObjectArray(count, pageClass, 0);
    int *childs = new int[count];
    childs[0] = count;
    int level = 0;
    for (int i = 0; i < count; i++) {
        GP<DjVmNav::DjVuBookMark> entry;
        nav->getBookMark(entry, i);
        GURL url = GURL::UTF8(entry->url,
                              djvu->doc->get_init_url()); // djvufileurl://0x7ca03d6000/document.djvu/p0268.djvu
        int page = djvu->doc->url_to_page(url);
        if (page == -1) {
            for (const char *ptr = entry->url; *ptr; ptr++) {
                if (*ptr == '#') {
                    ptr++;
                    url = GURL::UTF8(ptr, djvu->doc->get_init_url()); // #file.djvu or #345
                    page = djvu->doc->url_to_page(url);
                    if (page == -1) {
                        page = atoi(ptr) - 1;
                    }
                    break;
                }
            }
        }
        jobject title = 0;
        if (entry->displayname.is_valid())
            title = env->NewStringUTF((const char *) (entry->displayname));
        jobject bm = env->NewObject(pageClass, c, level, title, page);
        env->SetObjectArrayElement(ar, i, bm);
        env->DeleteLocalRef(title);
        env->DeleteLocalRef(bm);
        childs[level]--;
        if (childs[level] <= 0)
            level--;
        if (entry->count > 0) {
            level++;
            childs[level] = entry->count;
        }
    }
    delete[] childs;
    return ar;
}

}
